package de.lmu.dbs.jfeaturelib.features.surf;

import ij.IJ;
import ij.process.*;
// TODO detach from IJ, move IJ-related code into IJFacade
public class IntegralImage {

    /**
     * Should be set via setData() method.
     */
    private float[][] data;

    /**
     * Sets internal array to the argument
     * <code>a</code> and updates attributes width, height, maxX, maxY.
     */
    private void setData(float[][] a) {
        data = a;
        width = data.length;
        height = data[0].length;
        maxX = width - 1;
        maxY = height - 1;
    }
    private int width;
    private int height;
    /**
     * Max valid X coordinate.
     */
    private int maxX;
    /**
     * Max valid Y coordinate.
     */
    private int maxY;

    public float get(int x, int y) {
        return data[x][y];
    }

    // set() is not needed
    // public void set(int x, int y, float val) {
    // data[x][y] = val;
    // }
    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }

    public int getMaxX() {
        return maxX;
    }

    public int getMaxY() {
        return maxY;
    }

    // TODO: constructors for byte[] and float[] input images?
    /**
     * Creates the integral image of the source image. The order of coordinates
     * in the integral image is [x][y]. <p> The
     * <code>integral image</code> is an image where pixel values are sums of
     * pixel values above and to the left of the actual pixel (inclusive) from
     * the source image.
     */
    public IntegralImage(ImageProcessor src) { // TODO: remove this constructor in favor of IntegralImage(ImageProcessor, boolean)!!!
        // Get the source image as a new float[][] grayscale array
        // using the defautl ImageJ algorithm.
        // NB: in OpenSURF C# version following is used to convert from RGB to
        // grayscale:
        // luminance = (0.3 R + 0.59 G + 0.11 B) / 255.

        // ImageJ:
        // Weighting factors used by getPixelValue(), getHistogram() and
        // convertToByte().
        // Enable "Weighted RGB Conversion" in <i>Edit/Options/Conversions</i>
        // to use 0.299, 0.587 and 0.114.
        // private static double rWeight=1d/3d, gWeight=1d/3d, bWeight=1d/3d;
        // TODO: Call ColorProcessor.setWeightingFactors() ?

        // TODO: implement as int[][] or long[][]? Or as int[] or long[]?

        // NB: OpenSURF does   cvConvertScale( gray8, gray32, 1.0 / 255.0, 0 );
        // at converting to grayscale!


        // Initialize the instance variables
        setData(src.convertToByte(false).getFloatArray()); // [width][height]
        // [x][y]

        convertInternalBufferToIntegralImage();
    }

    /**
     * Compute the integral image.
     */
    private void convertInternalBufferToIntegralImage() {
        float rowSum = 0;

        // first row:
        for (int x = 0; x < width; x++) {
            rowSum += data[x][0];
            data[x][0] = rowSum;
        }

        // the rest:
        for (int y = 1; y < height; y++) {
            rowSum = 0;
            for (int x = 0; x < width; x++) {
                rowSum += data[x][y];
                data[x][y] = rowSum + data[x][y - 1];
            }
        }
    }

    public IntegralImage(ImageProcessor src, boolean weightedAndNormalizedConversion) {
        // TODO: make weightedAndNormalizedConversion the default and remove other constructors! 
        int width = src.getWidth();
        int height = src.getHeight();
        float[][] a = new float[width][height];
        float val, min = Float.MAX_VALUE, max = Float.MIN_VALUE;
        int i, x, y;
        float[] col;

        // Convert to float and compute min and max values

        if (src instanceof ByteProcessor || src instanceof ShortProcessor || src instanceof FloatProcessor) {
            for (i = 0, y = 0; y < height; y++) {
                for (x = 0; x < width; x++) {
                    val = src.getf(i++);
                    a[x][y] = val;
                    if (val < min) {
                        min = val;
                    } else if (val > max) {
                        max = val;
                    }
                }
            }

        } else if (src instanceof ColorProcessor) { // weighted conversion
            int intVal, r, g, b;
            float rw = 0.299f, gw = 0.587f, bw = 0.114f;

            for (i = 0, y = 0; y < height; y++) {
                for (x = 0; x < width; x++) {

                    intVal = src.get(i++);
                    r = (intVal & 0xff0000) >> 16;
                    g = (intVal & 0xff00) >> 8;
                    b = intVal & 0xff;
                    val = r * rw + g * gw + b * bw;

                    a[x][y] = val;
                    if (val < min) {
                        min = val;
                    } else if (val > max) {
                        max = val;
                    }
                }
            }

        } else {			// Should never happen.
            IJ.error("SURF: IntegralImage", "Unknown image type.\nCannot proceed.");
            return;
        }


        // Normalize values (i.e. scale max-min range to 0..1 range)

        float scale = 1f / (max - min);
        for (i = 0, x = 0; x < width; x++) {
            col = a[x];
            for (y = 0; y < height; y++) {
                val = col[y] - min;
                if (val < 0) {
                    val = 0;
                }
                val *= scale;
                if (val > 1) {
                    val = 1;
                }
                col[y] = val;
            }
        }

        setData(a);

        convertInternalBufferToIntegralImage();

    }

    /**
     * Computes the sum of pixels in an integral image
     * <code>img</code> within the rectangle specified by the top-left start
     * coordinate (inclusive) and size.<br>
     */
    float area(int x1, int y1, int rectWidth, int rectHeight) {
        x1--;
        y1--;                  //  A +--------+ B
        int x2 = x1 + rectWidth;     //    |        |        A(x1,y1)
        int y2 = y1 + rectHeight;    //  C +--------+ D

        // bounds check
        if (x1 > maxX) {
            x1 = maxX;
        }
        if (y1 > maxY) {
            y1 = maxY;
        }
        if (x2 > maxX) {
            x2 = maxX;
        }
        if (y2 > maxY) {
            y2 = maxY;
        }

        float A = (x1 < 0 || y1 < 0) ? 0 : data[x1][y1];
        float B = (x2 < 0 || y1 < 0) ? 0 : data[x2][y1];
        float C = (x1 < 0 || y2 < 0) ? 0 : data[x1][y2];
        float D = (x2 < 0 || y2 < 0) ? 0 : data[x2][y2];

        return D - B - C + A;

    }

    /**
     * A speed optimized version of {@link #area(FloatProcessor, int, int, int, int)}
     * without bounds check (for 0 < x1 < width and 0 < y1 < height).
     */
    float area2(int x1, int y1, int rectWidth, int rectHeight) {
        x1--;
        y1--;                  //  A +--------+ B
        int x2 = x1 + rectWidth;     //    |        |        A(x1,y1)
        int y2 = y1 + rectHeight;    //  C +--------+ D
        return data[x2][y2] - data[x2][y1] - data[x1][y2] + data[x1][y1]; // D - B - C + A 
    }
}
